import getopt
import os
import platform
import re
import sys
from typing import Iterable

import git

from dto.gitbranch import CheckResult, CheckType

ARG_DICT = {
    '-d': 'dir',
    '--dir': 'dir',
    '-b': 'branch',
    '--branch': 'branch',
    '-f': 'force',
    '--force': 'force',
    '--filter': 'filter',
    '--exclude': 'exclude',
    '--proxy': 'proxy',
    '--git-pull': 'git-pull',
    '--check': 'check',
    '--checkout': 'checkout',
    '--clone-to': 'clone-to',
    '--set-user': 'set-user',
    '--delete': 'delete',
}

ignore_paths = ['node_modules', 'build', 'target']


def main(argv):
    try:
        opts, args = getopt.getopt(argv, 'hd:b:f', [
            'help',
            'force',
            'git-pull',
            'dir=',
            'branch=',
            'filter=',
            'exclude=',
            'proxy=',
            'check',
            'checkout',
            'clone-to=',
            'set-user=',
            'delete',
        ])
    except getopt.GetoptError as e:
        print('错误的参数', argv, e.msg)
        print_help()
        exit(1)

    params = {}
    funcs = []
    for opt, arg in opts:
        if opt in ARG_DICT:
            params[ARG_DICT[opt]] = arg
        if opt in ('-h', '--help'):
            print_help()
            return
        elif opt in '--git-pull':
            funcs.append(opt_git_pull)
        elif opt in '--set-user':
            funcs.append(git_set_user)
        elif opt in ('-b', '--branch'):
            funcs.append(branch_option)
    if len(funcs) == 0:
        print('没有可用参数')
        print_help()
        return
    for fun in funcs:
        fun(params)

    system = platform.system()
    if system == 'Windows':
        os.system('pause')
    else:
        print('按Enter键继续...')
        os.system('read a')


def print_help():
    txt = '''
使用方法：
    -d --dir            指定操作目录，未指定则使用当前脚本的工作目录。
        --filter        过滤目标子文件夹。
        --exclude       排除目标子文件夹。
    --git-pull          拉取指定目录下所有的 Git 仓库。
    --clone-to          克隆指定目录下所有的 Git 仓库，需要指定分支，参数值为目标文件夹。
    --set-user          配置仓库用户，用户名与邮箱之间用“:”分开，例如：user:user@abc.com。
    -b --branch         指定 Git 分支。
        --check         查看指定分支是否存在。
        --checkout      拉取指定分支。
        --delete        删除指定本地分支。
            -f --force  强制删除指定分支。
    -h --help           查看帮助信息。

注意：
    短格式的参数前有一个空格，如：-d E:\\repositories
    长格式的参数前有一个等号，如：--branch=dev
    '''
    print(txt)


def list_dirs(directory, opt=None):
    if opt is None:
        opt = {}
    dirs = []
    if os.path.isdir(directory):
        for cd in os.listdir(directory):
            if cd.startswith('.') or cd in ignore_paths:
                continue
            path = os.path.join(directory, cd)
            if os.path.isdir(os.path.join(path, '.git')):
                dirs.append(path)
            else:
                dirs.extend(list_dirs(path))
    if 'filter' in opt:
        dirs = list(filter(lambda x: opt['filter'] in x, dirs))
    if 'exclude' in opt:
        dirs = list(filter(lambda x: re.search(opt['exclude'], x) is None, dirs))
    return dirs


def opt_git_pull(opt):
    if 'dir' not in opt:
        opt['dir'] = os.getcwd()
    print('>>> 拉取 Git 基础目录 %s' % opt['dir'])
    dirs = list_dirs(opt['dir'], opt)
    if len(dirs) == 0:
        print('<<< 没有需要拉取的 Git 仓库')
        return
    print('\n=============< 发现 %s 个 Git 仓库 >=============\n' % len(dirs))
    has_proxy = 'proxy' in opt and len(opt['proxy'].strip()) > 0
    if has_proxy:
        print('<<< 使用代理', opt['proxy'])
        os.system(f"git config --global http.proxy \"{opt['proxy']}\"")
    try:
        fail_paths = git_pull(dirs)
        while len(fail_paths) > 0:
            print('\n<<< 以下仓库拉取失败：')
            for path in fail_paths:
                print('  - %s' % path)
            print('\n')
            answer = input('是否重新拉取？(Y/N): ')
            if answer.lower() in ('y', 'yes'):
                fail_paths = git_pull(fail_paths)
            else:
                break
    finally:
        if has_proxy:
            os.system('git config --global --unset http.proxy')


def git_pull(dirs):
    retries = 3
    fail_paths = []
    for index, path in enumerate(dirs):
        print('>>> 拉取仓库 (%d/%d): %s' % (index + 1, len(dirs), path))
        success = False
        for times in range(0, retries):
            success = os.system('git -C ' + path + ' pull') == 0
            if success:
                os.system('git -C ' + path + ' remote prune origin')
                break
            print('<<< 第%d次拉取失败，重试...' % times)
        if not success:
            fail_paths.append(path)
            print('<<< 多次拉取失败，跳过该仓库拉取！')
        print('\n')
    print('==================< 拉取完成 >==================')
    return fail_paths


def git_set_user(opts):
    args = opts['set-user'].split(':')
    if len(args) >= 2:
        user, email = args
    else:
        user, email = None, None
    if user is None or len(user.strip()) == 0 or email is None or len(email.strip()) == 0:
        print('>>> 用户名或邮箱为空，无法设置用户')
        return

    if 'dir' not in opts:
        opts['dir'] = os.getcwd()
    print('>>> Git 仓库基础目录 %s' % opts['dir'])
    dirs = list_dirs(opts['dir'], opts)
    if len(dirs) == 0:
        print('<<< 没有需要操作的 Git 仓库')
        return
    print('\n=============< 找到 %s 个 Git 仓库 >=============\n' % len(dirs))
    for d in dirs:
        os.system('git -C ' + d + ' config user.name "' + user + '"')
        os.system('git -C ' + d + ' config user.email "' + email + '"')
        print(f'<<< {d} 用户设置成功"')


def branch_option(opts):
    branch = opts['branch']
    if branch is None or len(branch.strip()) == 0:
        print('>>> 操作的目标分支为空')
        return

    if 'dir' not in opts:
        opts['dir'] = os.getcwd()
    print('>>> Git 基础目录 %s' % opts['dir'])
    dirs = list_dirs(opts['dir'], opts)
    if len(dirs) == 0:
        print('<<< 没有需要操作的 Git 仓库')
        return
    print('\n=============< 发现 %s 个 Git 仓库 >=============\n' % len(dirs))

    if 'check' in opts:
        opt_git_check(dirs, branch, opts)
    elif 'checkout' in opts:
        opt_git_checkout(dirs, branch, opts)
    elif 'delete' in opts:
        opt_git_del_branch(dirs, branch, opts)
    elif 'clone-to' in opts:
        opt_git_clone_branch(dirs, branch, opts)
    else:
        print('无效的分支操作')


def git_find_branch(dirs, branch, opts) -> Iterable[CheckResult]:
    if branch.endswith('%'):
        branch = branch[:-1]
        for d in dirs:
            repo = git.Repo(d)
            if repo.head.reference.name.startswith(branch):
                yield CheckResult(repo, CheckType.CURRENT, repo.head.reference.name)
                continue
            hbs = list(filter(lambda x: x.name.startswith(branch), repo.heads))
            if len(hbs) > 0:
                yield CheckResult(repo, CheckType.LOCAL, hbs[0].name)
                continue
            has_remote = False
            for remote in repo.remotes:
                remote.fetch()
                remote_branch = f'{remote.name}/{branch}'
                rbs = list(filter(lambda x: x.name.startswith(remote_branch), remote.refs))
                if len(rbs) > 0:
                    has_remote = True
                    yield CheckResult(repo, CheckType.REMOTE, rbs[0].name, rbs[0])
                    break
            if has_remote:
                continue
            yield CheckResult(repo, CheckType.NONE)
    else:
        for d in dirs:
            repo = git.Repo(d)
            if repo.head.reference.name == branch:
                yield CheckResult(repo, CheckType.CURRENT, repo.head.reference.name)
                continue
            if branch in repo.heads:
                yield CheckResult(repo, CheckType.LOCAL, branch)
                continue
            has_remote = False
            for remote in repo.remotes:
                remote.fetch()
                remote_branch = f'{remote.name}/{branch}'
                rbs = list(filter(lambda x: x.name == remote_branch, remote.refs))
                if len(rbs) > 0:
                    has_remote = True
                    yield CheckResult(repo, CheckType.REMOTE, rbs[0].path, rbs[0])
                    break
            if has_remote:
                continue
            yield CheckResult(repo, CheckType.NONE)


def opt_git_check(dirs, branch, opts):
    results = git_find_branch(dirs, branch, opts)
    for result in results:
        match result.check_type:
            case CheckType.CURRENT:
                print(f'<<< {result.repo.working_dir.ljust(50, "-")} 当前分支 "{result.branch_name}"')
            case CheckType.LOCAL:
                print(f'<<< {result.repo.working_dir.ljust(50, "-")} 本地分支 "{result.branch_name}"')
            case CheckType.REMOTE:
                print(f'<<< {result.repo.working_dir.ljust(50, "-")} 远程分支 "{result.remote_branch.path}"')


def opt_git_checkout(dirs, branch, opts):
    results = git_find_branch(dirs, branch, opts)
    for result in results:
        repo = result.repo
        match result.check_type:
            case CheckType.NONE:
                print(f'<<< {repo.working_dir.ljust(50, '-')} 不存在匹配分支 "{branch}"')
            case CheckType.CURRENT:
                print(f'<<< {repo.working_dir.ljust(50, '-')} 当前分支 "{branch}"')
            case CheckType.LOCAL:
                repo.git.checkout(result.branch_name)
                print(f'<<< {repo.working_dir.ljust(50, "-")} 已切换分支 "{result.branch_name}"')
            case CheckType.REMOTE:
                rb = result.remote_branch
                repo_branch = rb.remote_head
                repo.create_head(repo_branch, rb)
                repo.heads[repo_branch].set_tracking_branch(rb)
                repo.heads[repo_branch].checkout()
                print(f'<<< {repo.working_dir.ljust(50, '-')} 已签出分支 "{rb.path}"')


def opt_git_clone_branch(dirs, branch, opts):
    if 'clone-to' not in opts:
        print('<<< 未指定克隆到目录')
    clone_to = opts['clone-to']
    if not os.path.isdir(clone_to):
        os.makedirs(clone_to, exist_ok=True)
    for d in dirs:
        repo = git.Repo(d)
        if len(repo.remotes) == 0:
            print(f'<<< {d.ljust(50, '-')} 没有远程仓库')
            continue
        for remote in repo.remotes:
            remote.fetch()
            remote_branch = f'{remote.name}/{branch}'

            rbs = list(filter(lambda x: x.name == remote_branch, remote.refs))
            if len(rbs) == 0 and branch.endswith('%'):
                rbs = list(filter(lambda x: x.name.startswith(remote_branch[:-1]), remote.refs))
            if len(rbs) == 0:
                print(f'<<< {d.ljust(50, '-')} 没有匹配分支 "{remote_branch}"')
                continue

            remote_branch = rbs[0]
            target_dir = os.path.join(clone_to, os.path.basename(d))
            git.Repo.clone_from(remote.url, target_dir, branch=remote_branch.remote_head)
            print(f'<<< {d.ljust(50, '-')} 克隆到 "{target_dir}"')
            break


def opt_git_del_branch(dirs, branch, opts):
    force = 'force' in opts and bool(opts['force'])
    for d in dirs:
        repo = git.Repo(d)
        if repo.head.reference.name == branch:
            repo.delete_head(branch, force=force)
            print(f'<<< {d.ljust(50, '-')} 删除当前分支 "{branch}"')
        elif branch in repo.heads:
            repo.delete_head(branch, force=force)
            print(f'<<< {d.ljust(50, '-')} 删除存在分支 "{branch}"')
        else:
            print(f'<<< {d.ljust(50, '-')} 没有本地分支 "{branch}"')


if __name__ == '__main__':
    main(sys.argv[1:])
